/*
 * Copyright (c) 2011, Andrew Cole
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice,
 * this list of conditions and the following disclaimer.
 *
 * Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation
 * and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
 * THE POSSIBILITY OF SUCH DAMAGE.
 */

package net.andycole.xmiparser.model;

import java.util.HashMap;
import java.util.ArrayList;

import org.apache.log4j.Logger;

import org.netbeans.api.mdr.MDRepository;
import org.netbeans.api.mdr.MDRManager;

import net.andycole.xmiparser.transfer.TransferClass;
import net.andycole.xmiparser.transfer.TransferOperation;
import net.andycole.xmiparser.transfer.TransferParameter;
import net.andycole.xmiparser.transfer.TransferAttribute;
import net.andycole.xmiparser.transfer.ASTypes;

import org.omg.uml.modelmanagement.Model;
import org.omg.uml.modelmanagement.UmlPackage;
import org.omg.uml.foundation.core.Attribute;
import org.omg.uml.foundation.core.Classifier;
import org.omg.uml.foundation.core.DataType;
import org.omg.uml.foundation.core.Abstraction;
import org.omg.uml.foundation.core.Generalization;
import org.omg.uml.foundation.core.Operation;
import org.omg.uml.foundation.core.Parameter;
import org.omg.uml.foundation.core.TaggedValue;
import org.omg.uml.foundation.datatypes.Multiplicity;
import org.omg.uml.foundation.datatypes.MultiplicityRange;
import org.omg.uml.foundation.core.TagDefinition;

import org.omg.uml.foundation.datatypes.VisibilityKind;
import org.omg.uml.foundation.datatypes.VisibilityKindEnum;
import org.omg.uml.foundation.datatypes.ScopeKind;
import org.omg.uml.foundation.datatypes.ScopeKindEnum;
import org.omg.uml.foundation.datatypes.ChangeableKind;
import org.omg.uml.foundation.datatypes.ChangeableKindEnum;
import org.omg.uml.foundation.datatypes.ParameterDirectionKindEnum;


/**
 * Provides the ability to add content to a UML metamodel that is compatible
 * with that of ArgoUML.
 * @author Andrew Cole
 */
public class ArgoModelImpl implements IModel
{
   private static final String UML_MM = "UML";
   private static final String UML_INSTANCE = "MyUMLExtent";

   private Logger LOG = Logger.getLogger(ArgoModelImpl.class);

   private MDRepository repository;
   // assume import successful until told otherwise
   private boolean importFail = false;
   private org.omg.uml.UmlPackage factory;
   private Model modelPackage;

   private HashMap<String, Classifier> classCache;
   private HashMap<String, UmlPackage> packageCache;
   //private HashMap<String, DataType> primitivesCache;
   private TagDefinition tagDef;
   private ArrayList<DeferredAttributeHolder> deferredAttributes;
   private ArrayList<DeferredParameterHolder> deferredParameters;

   private boolean useMultiplicities = false;

   public ArgoModelImpl()
   {
      super();
      init();
   }

   @Override
   public void setUseMultiplicities(boolean value)
   {
      useMultiplicities = value;
   }

   private void init()
   {
      repository = MDRManager.getDefault().getDefaultRepository();
      classCache = new HashMap<String, Classifier>();
      packageCache = new HashMap<String, UmlPackage>();
      //primitivesCache = new HashMap<String, DataType>();
      deferredAttributes = new ArrayList<DeferredAttributeHolder>();
      deferredParameters = new ArrayList<DeferredParameterHolder>();

      // start a transaction
      boolean fail = true;
      org.omg.uml.UmlPackage uml = null;
      repository.beginTrans(true);
      try {
         // create a new MOF extent
         RepositoryManager manager = new RepositoryManager(repository);
         manager.setMMDetails(UML_MM, "/01-02-15_Diff.xml", "UML");
         uml = (org.omg.uml.UmlPackage) manager.createDatamodel(UML_INSTANCE);

         modelPackage = uml.getModelManagement().getModel().createModel();
         modelPackage.setName("AS3Model");
         factory = uml;
         createPrimitives();
         fail = false;
      } catch (Exception e) {
//         System.err.println("Fatal error reading XMI for UML extent.");
//         e.printStackTrace(System.err);
         LOG.error("Fatal error reading XMI for UML extent.", e);
      } finally {
         // commit/rollback the transaction
         repository.endTrans(fail);
      }
   }

   @Override
   public void startTransaction(boolean isWrite)
   {
      repository.beginTrans(isWrite);
   }

   @Override
   public void endTransaction()
   {
      // iterate over the model elements that do not yet reference an NBM
      // type. (Looking it up using the associated qualified class name)
      for (DeferredAttributeHolder holder : deferredAttributes)
      {
         Classifier type = getClassByName(holder.qualifiedClassName, false);
         Attribute element = holder.element;
         element.setType(type);
      }
      for (DeferredParameterHolder holder : deferredParameters)
      {
         Classifier type = getClassByName(holder.qualifiedClassName, false);
         Parameter element = holder.element;
         element.setType(type);
      }

      // commit/rollback the transaction
      repository.endTrans(importFail);

      //MDRManager.getDefault().shutdownAll();
   }

   @Override
   public void addClass(TransferClass newClass)
   {
      String pkgName = newClass.getPackagePath();
      addClassByName(pkgName, newClass);
   }

   private Classifier getClassByName(String  qualClassName,
                                     boolean isInterface)
   {
      Classifier cachedClass = classCache.get(qualClassName);

      if (cachedClass == null)
      {
         String className = qualClassName;
         String pkgName = "";
         int lastSepIndex = qualClassName.lastIndexOf(".");
         if (lastSepIndex > 0)
         {
            className = qualClassName.substring(lastSepIndex+1);
            pkgName = qualClassName.substring(0, lastSepIndex);
         }
         UmlPackage parentPkg = getParentPackage(pkgName);
         // create class ModelElement and set in cache
         if (isInterface)
            cachedClass = factory.getCore().getInterface().createInterface();
         else
            cachedClass = factory.getCore().getUmlClass().createUmlClass();
         parentPkg.getOwnedElement().add(cachedClass);
         classCache.put(qualClassName, cachedClass);
         cachedClass.setName(className);
      }
      return cachedClass;
   }

   private void addClassByName(String pkgName,
                               TransferClass newClass)
   {
      String qualifiedClassName = newClass.getQualifiedClassName();
      Classifier cachedClass = classCache.get(qualifiedClassName);

      if (cachedClass == null)
      {
         // create/get parent packages
         // get parent package
         UmlPackage parentPkg = getParentPackage(pkgName);
         // create class ModelElement and set in cache
         if (newClass.isInterface())
            cachedClass = factory.getCore().getInterface().createInterface();
         else
            cachedClass = factory.getCore().getUmlClass().createUmlClass();
         parentPkg.getOwnedElement().add(cachedClass);
         classCache.put(qualifiedClassName, cachedClass);
      }

      cachedClass.setName(newClass.getClassName());
      
      // set class attributes/operations etc.
      for (TransferOperation method : newClass.getMethods())
      {
         addMethodToClass(method, cachedClass);
      }

      for (TransferAttribute attr : newClass.getAttributes())
      {
         addAttributeToClass(attr, cachedClass);
      }

      setSuperclasses(newClass, cachedClass);

//      newClass.getDependencies();
//      newClass.getAssociations();
      
      if (newClass.getAnnotation() != null &&
          newClass.getAnnotation().length() > 0)
      {
         TaggedValue tvalue = factory.getCore().getTaggedValue().createTaggedValue();
         tvalue.getDataValue().add(newClass.getAnnotation());
         tvalue.setType(tagDef);
         tvalue.setModelElement(cachedClass);
      }
   }

   private void setSuperclasses(TransferClass newClass, Classifier cachedClass)
   {
      // extends (generalization)
      // get the parent class from the cache
      String baseClassName = newClass.getExtends();
      if (baseClassName != null && baseClassName.length() > 0)
      {
         Classifier baseClass = getClassByName(baseClassName,
                                               false);
         Generalization gens = factory.getCore().getGeneralization().createGeneralization();
         gens.setParent(baseClass);
         gens.setChild(cachedClass);
         if (baseClass.getNamespace() != null)
            baseClass.getNamespace().getOwnedElement().add(gens);
      }

      // realization (abstraction)
      // iterate over list of implemented interfaces
      for (String interfaceClassName : newClass.getImplementsArray())
      {
         Classifier inter = getClassByName(interfaceClassName,
                                           true);
         Abstraction abs = factory.getCore().getAbstraction().createAbstraction();
         abs.getClient().add(cachedClass);
         abs.getSupplier().add(inter);
         if (inter.getNamespace() != null)
            inter.getNamespace().getOwnedElement().add(abs);
      }

   }

   private void addMethodToClass(TransferOperation tMethod, Classifier cachedClass)
   {
      Operation op = factory.getCore().getOperation().createOperation();
      op.setName(tMethod.getName());
      op.setOwner(cachedClass);

      for (TransferParameter tParam : tMethod.getParameters())
      {
         Parameter param = factory.getCore().getParameter().createParameter();
         param.setName(tParam.getName());
         op.getParameter().add(param);

         // Do not set type here - this is performed on a second pass
         // through all typed elements
         deferredParameters.add(new DeferredParameterHolder(param, tParam.getType()));
      }

      op.setVisibility(getVisibilityKind(tMethod.getAccessLevel()));
      op.setOwnerScope(getScopeKind(tMethod.isStatic()));

      if (tMethod.getAnnotation() != null &&
          tMethod.getAnnotation().length() > 0)
      {
         TaggedValue tvalue = factory.getCore().getTaggedValue().createTaggedValue();
         tvalue.getDataValue().add(tMethod.getAnnotation());
         
         tvalue.setType(tagDef);
         tvalue.setModelElement(op);
      }
      
      String resultTypeName = tMethod.getResultType();
      if (resultTypeName != null && resultTypeName.length() > 0)
      {
         Parameter outParam = factory.getCore().getParameter().createParameter();
         op.getParameter().add(outParam);

         // Do not set type here - this is performed on a second pass
         // through all typed elements
         deferredParameters.add(new DeferredParameterHolder(outParam, resultTypeName));
         outParam.setKind(ParameterDirectionKindEnum.PDK_RETURN);
      }
      
   }

   private VisibilityKind getVisibilityKind(String visibility)
   {
      VisibilityKind retKind = VisibilityKindEnum.VK_PRIVATE;
      if ("private".equals(visibility))
         retKind = VisibilityKindEnum.VK_PRIVATE;
      else if ("protected".equals(visibility))
         retKind = VisibilityKindEnum.VK_PROTECTED;
      else if ("public".equals(visibility))
         retKind = VisibilityKindEnum.VK_PUBLIC;
      return retKind;
   }

   private ScopeKind getScopeKind(boolean isStatic)
   {
      ScopeKind retKind = ScopeKindEnum.SK_INSTANCE;
      if (isStatic)
         retKind = ScopeKindEnum.SK_CLASSIFIER;
      return retKind;
   }

   private ChangeableKind getChangeable(boolean isConst)
   {
      ChangeableKind retKind = ChangeableKindEnum.CK_CHANGEABLE;
      if (isConst)
         retKind = ChangeableKindEnum.CK_FROZEN;
      return retKind;
   }

   private void addAttributeToClass(TransferAttribute tAttribute,
                                    Classifier cachedClass)
   {
      Attribute att = factory.getCore().getAttribute().createAttribute();
      att.setName(tAttribute.getName());

      // Do not set type here - this is performed on a second pass
      // through all typed elements
      
      att.setOwner(cachedClass);
      att.setChangeability(getChangeable(tAttribute.isConst()));
      att.setVisibility(getVisibilityKind(tAttribute.getAccessLevel()));
      att.setOwnerScope(getScopeKind(tAttribute.isStatic()));
      //tAttribute.isFinal();

      if (tAttribute.getMetaType() != null && useMultiplicities)
      {
         Multiplicity defaultMult = factory.getDataTypes().getMultiplicity().createMultiplicity();
         att.setMultiplicity(defaultMult);
         MultiplicityRange range = factory.getDataTypes().getMultiplicityRange().createMultiplicityRange();
         range.setLower(0);
         range.setUpper(-1);
         defaultMult.getRange().add(range);
         deferredAttributes.add(new DeferredAttributeHolder(att, tAttribute.getMetaType()));
      }
      else
      {
         deferredAttributes.add(new DeferredAttributeHolder(att, tAttribute.getType()));
      }

      if (tAttribute.getAnnotation() != null &&
          tAttribute.getAnnotation().length() > 0)
      {
         TaggedValue tvalue = factory.getCore().getTaggedValue().createTaggedValue();
         tvalue.getDataValue().add(tAttribute.getAnnotation());
         tvalue.setType(tagDef);
         tvalue.setModelElement(att);
      }
   }

   private void createPrimitives()
   {
      for (ASTypes type : ASTypes.values())
      {
         DataType dt = factory.getCore().getDataType().createDataType();
         String typeName = type.toString();
         dt.setName(typeName);
         modelPackage.getOwnedElement().add(dt);
         classCache.put(typeName, dt);
      }

      // create TagDefinition for documentation
      tagDef = factory.getCore().getTagDefinition().createTagDefinition();
      tagDef.setTagType("String");
      tagDef.setName("documentation");
      Multiplicity mult = factory.getDataTypes().getMultiplicity().createMultiplicity();
      MultiplicityRange range = factory.getDataTypes().getMultiplicityRange().createMultiplicityRange();
      range.setLower(0);
      range.setUpper(1);
      mult.getRange().add(range);
      tagDef.setMultiplicity(mult);
   }

   @Override
   public Object getBasePackage()
   {
      return factory;
   }

   private UmlPackage getParentPackage(String pkgPath)
   {
      UmlPackage currentPackage = modelPackage;
      String[] pkgList = null;
      if (pkgPath.length() > 0)
      {
         pkgList = pkgPath.split("\\.");
         for (String pkgName : pkgList)
         {
            UmlPackage parentPkg = currentPackage;
            currentPackage = packageCache.get(pkgName);
            if (currentPackage == null)
            {
               currentPackage = factory.getModelManagement().getUmlPackage().createUmlPackage();
               currentPackage.setName(pkgName);
               parentPkg.getOwnedElement().add(currentPackage);
               packageCache.put(pkgName, currentPackage);
            }
         }
      }

      return currentPackage;
   }

   private class DeferredParameterHolder
   {
      public DeferredParameterHolder(Parameter element,
                                     String qualifiedClassName)
      {
         this.element = element;
         this.qualifiedClassName = qualifiedClassName;
      }
      
      public Parameter element;
      public String qualifiedClassName;
   }

   private class DeferredAttributeHolder
   {
      public DeferredAttributeHolder(Attribute element,
                                     String qualifiedClassName)
      {
         this.element = element;
         this.qualifiedClassName = qualifiedClassName;
      }
      public Attribute element;
      public String qualifiedClassName;
   }
}
